// This Pine Script™ code is subject to the terms of
// the Mozilla Public License 2.0:
// https://mozilla.org/MPL/2.0/
//
// VIRU TREND CONFIRMATION
// Combined and modified for Virendra Pandey | Codeviser
//
// Original components:
// ATR HEMA implementation: © SeerQuant
// Trend Reversal oscillator implementation: © AlgoAlpha
//
// Modifications:
// Combined direction agreement, frozen setup-candle levels,
// close-confirmed breakout signals, setup expiry and dashboard.

//@version=6
indicator(
     title = "Viru Trend Confirmation",
     shorttitle = "Viru TC",
     overlay = false,
     max_labels_count = 100)

//====================================================================
// INPUTS
//====================================================================
gHema = "1. Trend Engine"

alphaL = input.int(
     20, "Alpha Length",
     minval = 1, group = gHema)

gammaL = input.int(
     20, "Gamma Length",
     minval = 1, group = gHema)

atrMult = input.float(
     0.04, "Neutral Zone Multiplier",
     minval = 0, step = 0.01, group = gHema)

atrLength = input.int(
     14, "ATR Length",
     minval = 1, group = gHema)

src = input.source(
     hl2, "Trend Source", group = gHema)

gOsc = "2. Momentum Confirmation"

oscPeriod = input.int(
     20, "Oscillator Length",
     minval = 1, group = gOsc)

sampleLimit = input.int(
     200, "Stored Completed Trend Durations",
     minval = 20, maxval = 1000, group = gOsc)

showLevels = input.bool(
     true, "Show Duration Levels", group = gOsc)

gConfirmation = "3. Breakout Confirmation"

confirmationBars = input.int(
     3, "Bars Allowed After Setup",
     minval = 1, maxval = 20,
     group = gConfirmation,
     tooltip = "Excludes the setup candle. A value of 3 allows " +
         "confirmation on any of the next three closed candles.")

showSetupLevels = input.bool(
     true, "Show Pending Setup High / Low",
     group = gConfirmation)

showSetupMarkers = input.bool(
     false, "Show Setup Markers",
     group = gConfirmation)

gStyle = "4. Appearance"

bullColor = input.color(
     #089981, "Bullish Color", group = gStyle)

bearColor = input.color(
     #D64252, "Bearish Color", group = gStyle)

paintBars = input.bool(
     false, "Color Price Candles", group = gStyle)

showHema = input.bool(
     true, "Show Trend Line on Price Chart", group = gStyle)

showDashboard = input.bool(
     true, "Show Dashboard", group = gStyle)

//====================================================================
// TREND ENGINE
// ATR HEMA implementation derived from SeerQuant.
// Original recursive calculation retained.
//====================================================================
float alpha = 2.0 / (alphaL + 1.0)
float gamma = 2.0 / (gammaL + 1.0)

var float b = 0.0
var float hema = 0.0

hema := (1.0 - alpha) *
     (nz(hema[1]) + nz(b[1], src)) + alpha * src

b := (1.0 - gamma) * nz(b[1]) +
     gamma * (hema - nz(hema[1]))

float atrValue = ta.atr(atrLength)
float neutralThreshold = atrValue * atrMult
float hemaChange = hema - hema[1]

// Direction is updated only after candle close.
// Inside the neutral zone, retain the previous direction.
var int hemaDirection = 0

if barstate.isconfirmed and
     not na(hemaChange) and not na(neutralThreshold)

    if math.abs(hemaChange) >= neutralThreshold
        if hemaChange > 0
            hemaDirection := 1
        else if hemaChange < 0
            hemaDirection := -1

color hemaColor = hemaDirection == 1 ? bullColor :
     hemaDirection == -1 ? bearColor : color.gray

//====================================================================
// MOMENTUM ENGINE
// AO + RSI-like implementation derived from AlgoAlpha.
//====================================================================
float midpointPrice = hl2
float shortSma = ta.sma(midpointPrice, 5)
float longSma = ta.sma(midpointPrice, 34)
float amazingOsc = shortSma - longSma

float oscillatorChange = ta.change(amazingOsc)

float rise = ta.rma(
     math.max(oscillatorChange, 0), oscPeriod)

float fall = ta.rma(
     -math.min(oscillatorChange, 0), oscPeriod)

float customRsi = (
     fall == 0 ? 100.0 :
     rise == 0 ? 0.0 :
     100.0 - 100.0 / (1.0 + rise / fall)) - 50.0

var int oscillatorDirection = 0

if barstate.isconfirmed
    oscillatorDirection := na(customRsi) ? 0 :
         customRsi > 0 ? 1 :
         customRsi < 0 ? -1 : 0

//====================================================================
// DURATION STATISTICS
// The first partially observed regime is excluded.
// These statistics do not control entries.
//====================================================================
var array<int> durations = array.new_int()

var int regimeDirection = 0
var int regimeStart = na
var bool fullRegime = false

if barstate.isconfirmed and oscillatorDirection != 0
    if regimeDirection == 0
        regimeDirection := oscillatorDirection
        regimeStart := bar_index

    else if oscillatorDirection != regimeDirection
        if fullRegime
            array.unshift(
                 durations, bar_index - regimeStart)

            if array.size(durations) > sampleLimit
                array.pop(durations)

        regimeDirection := oscillatorDirection
        regimeStart := bar_index
        fullRegime := true

int duration = na(regimeStart) ?
     na : bar_index - regimeStart

float basis = array.size(durations) > 0 ?
     array.avg(durations) : na

float deviation = array.size(durations) > 1 ?
     array.stdev(durations) : na

//====================================================================
// COMBINED AGREEMENT
//====================================================================
int agreement =
     hemaDirection == 1 and oscillatorDirection == 1 ? 1 :
     hemaDirection == -1 and oscillatorDirection == -1 ? -1 : 0

//====================================================================
// SETUP STATE
//
// A fresh agreement creates one setup.
// Setup high and low are frozen.
// Signals cannot occur on the setup candle.
// A cancelled / expired setup requires a fresh agreement.
// No forced BUY / SELL alternation.
//====================================================================
var int previousAgreement = 0

var bool pending = false
var int setupDirection = 0
var int setupBar = na

var float setupHigh = na
var float setupLow = na

var int lastSignal = 0
var string lastEvent = "Waiting for agreement"

bool buySignal = false
bool sellSignal = false
bool bullSetup = false
bool bearSetup = false

//====================================================================
// CLOSED-CANDLE CONFIRMATION
//====================================================================
if barstate.isconfirmed

    // Manage an existing setup first.
    if pending
        int setupAge = bar_index - setupBar

        bool agreementBroken =
             agreement != setupDirection

        // Opposite-extreme wick breach invalidates the setup.
        bool invalidated = setupDirection == 1 ?
             low < setupLow :
             high > setupHigh

        bool expired =
             setupAge > confirmationBars

        // Cancellation takes priority over breakout confirmation.
        if agreementBroken
            pending := false
            lastEvent := "Cancelled: agreement broken"

        else if invalidated
            pending := false
            lastEvent := setupDirection == 1 ?
                 "Cancelled: setup low broken" :
                 "Cancelled: setup high broken"

        else if expired
            pending := false
            lastEvent := "Setup expired"

        else if setupAge >= 1
            bool breakoutConfirmed = setupDirection == 1 ?
                 close > setupHigh :
                 close < setupLow

            if breakoutConfirmed
                if setupDirection == 1
                    buySignal := true
                    lastSignal := 1
                    lastEvent := "BUY confirmed"
                else
                    sellSignal := true
                    lastSignal := -1
                    lastEvent := "SELL confirmed"

                pending := false

    // Arm a new setup after a fresh agreement.
    // Confirmation cannot occur until a subsequent candle.
    bool freshAgreement = agreement != 0 and
         agreement != previousAgreement

    if freshAgreement
        pending := true
        setupDirection := agreement
        setupBar := bar_index
        setupHigh := high
        setupLow := low

        bullSetup := agreement == 1
        bearSetup := agreement == -1

        lastEvent := agreement == 1 ?
             "Bullish setup armed" :
             "Bearish setup armed"

    previousAgreement := agreement

//====================================================================
// PRICE CHART
//====================================================================
plot(
     showHema ? hema : na,
     title = "HEMA Trend Line",
     color = hemaColor,
     linewidth = 3,
     force_overlay = true)

barcolor(paintBars ? hemaColor : na)

plot(
     showSetupLevels and pending ? setupHigh : na,
     title = "Pending Setup High",
     color = bullColor,
     linewidth = 2,
     style = plot.style_linebr,
     force_overlay = true)

plot(
     showSetupLevels and pending ? setupLow : na,
     title = "Pending Setup Low",
     color = bearColor,
     linewidth = 2,
     style = plot.style_linebr,
     force_overlay = true)

plotshape(
     showSetupMarkers and bullSetup,
     title = "Bullish Setup",
     text = "SET",
     style = shape.circle,
     location = location.belowbar,
     color = color.new(bullColor, 45),
     textcolor = bullColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showSetupMarkers and bearSetup,
     title = "Bearish Setup",
     text = "SET",
     style = shape.circle,
     location = location.abovebar,
     color = color.new(bearColor, 45),
     textcolor = bearColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     buySignal,
     title = "Viru Confirmed BUY",
     text = "BUY",
     style = shape.labelup,
     location = location.belowbar,
     color = bullColor,
     textcolor = color.white,
     size = size.small,
     force_overlay = true)

plotshape(
     sellSignal,
     title = "Viru Confirmed SELL",
     text = "SELL",
     style = shape.labeldown,
     location = location.abovebar,
     color = bearColor,
     textcolor = color.white,
     size = size.small,
     force_overlay = true)

//====================================================================
// DURATION PANE
// Height represents elapsed duration, not momentum strength.
//====================================================================
color durationColor = oscillatorDirection == 1 ? bullColor :
     oscillatorDirection == -1 ? bearColor : color.gray

plot(
     duration,
     "Trend Duration",
     color = color.new(durationColor, 70),
     style = plot.style_columns)

plot(
     duration * 0.8,
     "Duration 80%",
     color = color.new(durationColor, 65),
     style = plot.style_columns)

plot(
     duration * 0.6,
     "Duration 60%",
     color = color.new(durationColor, 55),
     style = plot.style_columns)

plot(
     duration * 0.4,
     "Duration 40%",
     color = color.new(durationColor, 40),
     style = plot.style_columns)

plot(
     duration * 0.2,
     "Duration 20%",
     color = color.new(durationColor, 35),
     style = plot.style_columns)

plot(
     showLevels ? basis : na,
     "Average Duration",
     color = color.gray)

pUpper = plot(
     showLevels ? basis + deviation : na,
     "Duration +1 SD",
     color = color.new(color.orange, 35))

plot(
     showLevels ? basis + deviation * 2 : na,
     "Duration +2 SD",
     color = color.new(color.orange, 50))

plot(
     showLevels ? basis + deviation * 3 : na,
     "Duration +3 SD",
     color = color.new(color.orange, 65))

pLower = plot(
     showLevels ? math.max(0, basis - deviation) : na,
     "Duration -1 SD",
     color = color.new(color.blue, 45))

fill(
     pUpper, pLower,
     color = color.new(color.blue, 94))

plot(
     customRsi,
     "Momentum Oscillator",
     display = display.data_window)

//====================================================================
// VIRU DASHBOARD
//====================================================================
var table dashboard = table.new(
     position.top_right,
     2, 7,
     bgcolor = color.new(#101826, 5),
     border_width = 1,
     border_color = color.new(color.gray, 70))

if barstate.isfirst
    table.merge_cells(dashboard, 0, 0, 1, 0)

if barstate.islast and showDashboard

    string hemaText = hemaDirection == 1 ? "BULLISH" :
         hemaDirection == -1 ? "BEARISH" : "WAIT"

    string oscText = oscillatorDirection == 1 ? "BULLISH" :
         oscillatorDirection == -1 ? "BEARISH" : "NEUTRAL"

    string setupText = pending ?
         (setupDirection == 1 ? "BUY PENDING" : "SELL PENDING") :
         "NO PENDING SETUP"

    string triggerText = pending ?
         str.tostring(
             setupDirection == 1 ? setupHigh : setupLow,
             format.mintick) :
         "—"

    table.cell(
         dashboard, 0, 0,
         "VIRU TREND CONFIRMATION",
         text_color = color.white,
         bgcolor = #172A45,
         text_size = size.small)

    table.cell(
         dashboard, 0, 1, "Trend",
         text_color = color.silver)

    table.cell(
         dashboard, 1, 1, hemaText,
         text_color = hemaColor)

    table.cell(
         dashboard, 0, 2, "Momentum",
         text_color = color.silver)

    table.cell(
         dashboard, 1, 2, oscText,
         text_color = durationColor)

    table.cell(
         dashboard, 0, 3, "Setup",
         text_color = color.silver)

    table.cell(
         dashboard, 1, 3, setupText,
         text_color = pending ?
             (setupDirection == 1 ? bullColor : bearColor) :
             color.silver)

    table.cell(
         dashboard, 0, 4, "Breakout Close Level",
         text_color = color.silver)

    table.cell(
         dashboard, 1, 4, triggerText,
         text_color = color.white)

    table.cell(
         dashboard, 0, 5, "Last Event",
         text_color = color.silver)

    table.cell(
         dashboard, 1, 5, lastEvent,
         text_color = color.white)

    table.cell(
         dashboard, 0, 6, "Last Signal",
         text_color = color.silver)

    table.cell(
         dashboard, 1, 6,
         lastSignal == 1 ? "BUY" :
         lastSignal == -1 ? "SELL" : "NONE",
         text_color = lastSignal == 1 ? bullColor :
             lastSignal == -1 ? bearColor : color.silver)

//====================================================================
// ALERTS
//====================================================================
alertcondition(
     buySignal,
     title = "Viru Trend Confirmation — BUY",
     message = "VIRU BUY: Bullish trend and momentum confirmed; candle closed above setup high | {{ticker}} | {{interval}} | Close {{close}}")

alertcondition(
     sellSignal,
     title = "Viru Trend Confirmation — SELL",
     message = "VIRU SELL: Bearish trend and momentum confirmed; candle closed below setup low | {{ticker}} | {{interval}} | Close {{close}}")